package com.youth.framework.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;

public class ResolverUtil<T> {
	private static final Logger log = Logger.getLogger(ResolverUtil.class);

	private static final byte[] JAR_MAGIC = { 80, 75, 3, 4 };

	private static final Pattern JAVA_IDENTIFIER_PATTERN = Pattern.compile("\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*");

	private Set<Class<T>> matches = new HashSet<Class<T>>();
	private ClassLoader classloader = ClassUtils.getDefaultClassLoader();

	public Set<Class<T>> getClasses() {
		return this.matches;
	}

	public ClassLoader getClassLoader() {
		return this.classloader == null ? Thread.currentThread().getContextClassLoader() : this.classloader;
	}

	public ResolverUtil<T> findImplementations(Class<?> parent,
			String[] packageNames) {
		if (packageNames == null)
			return this;

		Test test = new IsA(parent);
		for (String pkg : packageNames) {
			find(test, pkg);
		}

		return this;
	}

	public ResolverUtil<T> findAnnotated(
			Class<? extends Annotation> annotation, String[] packageNames) {
		if (packageNames == null)
			return this;

		Test test = new AnnotatedWith(annotation);
		for (String pkg : packageNames) {
			find(test, pkg);
		}

		return this;
	}

	public ResolverUtil<T> find(Test test, String packageName) {
		String path = getPackagePath(packageName);
		try {
			ClassLoader loader = this.getClass().getClassLoader();
			List<URL> urls = Collections.list(loader.getResources(path));
			for (URL url : urls) {
				List<String> children = listClassResources(url, path);
				for (String child : children) {
					addIfMatching(test, child);
				}
			}
		} catch (IOException ioe) {
			log.error("Could not read package: " + packageName + " -- ", ioe);
		}

		return this;
	}

	protected List<String> listClassResources(URL url, String path)
			throws IOException {
		log.debug("Listing classes in " + url);

		InputStream is = null;
		try {
			List<String> resources = new ArrayList<String>();

			URL jarUrl = findJarForResource(url, path);
			List<String> children;
			String prefix;
			if (jarUrl != null) {
				is = jarUrl.openStream();
				resources = listClassResources(new JarInputStream(is), path);
			} else {
				children = new ArrayList<String>();
				try {
					if (isJar(url)) {
						is = url.openStream();
						JarInputStream jarInput = new JarInputStream(is);
						JarEntry entry;
						while ((entry = jarInput.getNextJarEntry()) != null) {
							log.trace("Jar entry: " + entry.getName());
							if (isRelevantResource(entry.getName())) {
								children.add(entry.getName());
							}
						}
					} else {
						is = url.openStream();
						BufferedReader reader = new BufferedReader(
								new InputStreamReader(is));
						String line;
						while ((line = reader.readLine()) != null) {
							log.trace("Reader entry: " + line);
							if (isRelevantResource(line)) {
								children.add(line);
							}

						}

					}

				} catch (FileNotFoundException e) {
					if ("file".equals(url.getProtocol())) {
						File file = new File(url.getFile());
						log
								.trace("Listing directory "
										+ file.getAbsolutePath());
						if (file.isDirectory())
							children = Arrays.asList(file
									.list(new FilenameFilter() {
										public boolean accept(File dir,
												String name) {
											return ResolverUtil.this
													.isRelevantResource(name);
										}
									}));
					} else {
						throw e;
					}

				}

				prefix = url.toExternalForm();
				if (!prefix.endsWith("/")) {
					prefix = prefix + "/";
				}

				for (String child : children) {
					String resourcePath = path + "/" + child;
					if (child.endsWith(".class")) {
						log.trace("Found class file: " + resourcePath);
						resources.add(resourcePath);
					} else {
						URL childUrl = new URL(prefix + child);
						resources.addAll(listClassResources(childUrl,
								resourcePath));
					}
				}
			}

			return resources;
		} finally {
			try {
				is.close();
			} catch (Exception e) {
			}
		}
	}

	protected List<String> listClassResources(JarInputStream jar, String path)
			throws IOException {
		if (!path.startsWith("/"))
			path = "/" + path;
		if (!path.endsWith("/")) {
			path = path + "/";
		}

		List<String> resources = new ArrayList<String>();
		JarEntry entry;
		while ((entry = jar.getNextJarEntry()) != null) {
			if (!entry.isDirectory()) {
				String name = entry.getName();
				if (!name.startsWith("/")) {
					name = "/" + name;
				}

				if ((name.endsWith(".class")) && (name.startsWith(path))) {
					log.trace("Found class file: " + name);
					resources.add(name.substring(1));
				}
			}
		}
		return resources;
	}

	protected URL findJarForResource(URL url, String path)
			throws MalformedURLException {
		log.trace("Find JAR URL: " + url);
		StringBuilder jarUrl;
		try {
			while (true) {
				url = new URL(url.getFile());
				log.trace("Inner URL: " + url);
			}

		} catch (MalformedURLException e) {
			jarUrl = new StringBuilder(url.toExternalForm());
			int index = jarUrl.lastIndexOf(".jar");
			if (index >= 0) {
				jarUrl.setLength(index + 4);
				log.trace("Extracted JAR URL: " + jarUrl);
			} else {
				log.trace("Not a JAR: " + jarUrl);
				return null;
			}
		}
		try {
			URL testUrl = new URL(jarUrl.toString());
			if (isJar(testUrl)) {
				return testUrl;
			}

			log.trace("Not a JAR: " + jarUrl);
			jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
			File file = new File(jarUrl.toString());

			if (file.exists()) {
				log.trace("Trying real file: " + file.getAbsolutePath());
				testUrl = file.toURI().toURL();
				if (isJar(testUrl)) {
					return testUrl;
				}
			}
		} catch (MalformedURLException e) {
			log.warn("Invalid JAR URL: " + jarUrl);
		}

		log.trace("Not a JAR: " + jarUrl);
		return null;
	}

	protected String getPackagePath(String packageName) {
		return packageName == null ? null : packageName.replace('.', '/');
	}

	protected boolean isRelevantResource(String resourceName) {
		return (resourceName != null)
				&& ((resourceName.endsWith(".class")) || (JAVA_IDENTIFIER_PATTERN
						.matcher(resourceName).matches()));
	}

	protected boolean isJar(URL url) {
		return isJar(url, new byte[JAR_MAGIC.length]);
	}

	protected boolean isJar(URL url, byte[] buffer) {
		InputStream is = null;
		try {
			is = url.openStream();
			is.read(buffer, 0, JAR_MAGIC.length);
			if (Arrays.equals(buffer, JAR_MAGIC)) {
				log.debug("Found JAR: " + url);
				return true;
			}
		} catch (Exception e) {
		} finally {
			try {
				is.close();
			} catch (Exception e) {
			}
		}
		return false;
	}

	@SuppressWarnings("unchecked")
	protected void addIfMatching(Test test, String fqn) {
		try {
			String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
			ClassLoader loader = getClassLoader();
			log.trace("Checking to see if class " + externalName + " matches criteria [" + test + "]");

			Class<T> type = (Class<T>)loader.loadClass(externalName);
			if (test.matches(type))
				this.matches.add(type);
		} catch (Throwable t) {
			log.warn("Could not examine class '" + fqn + "'" + " due to a "
					+ t.getClass().getName() + " with message: "
					+ t.getMessage());
		}
	}

	public static class AnnotatedWith implements ResolverUtil.Test {
		private Class<? extends Annotation> annotation;

		public AnnotatedWith(Class<? extends Annotation> annotation) {
			this.annotation = annotation;
		}

		@SuppressWarnings("unchecked")
		public boolean matches(Class type) {
			return (type != null) && (type.isAnnotationPresent(this.annotation));
		}

		public String toString() {
			return "annotated with @" + this.annotation.getSimpleName();
		}
	}

	public static class IsA implements ResolverUtil.Test {
		private Class<?> parent;

		public IsA(Class<?> parentType) {
			this.parent = parentType;
		}

		@SuppressWarnings("unchecked")
		public boolean matches(Class type) {
			return (type != null) && (this.parent.isAssignableFrom(type));
		}

		public String toString() {
			return "is assignable to " + this.parent.getSimpleName();
		}
	}

	public static abstract interface Test {
		public abstract boolean matches(Class<?> paramClass);
	}
}
